"use strict";

var C = require('../common'),
    uid = require('uid-safe').sync,
    expressSession = require('express-session');

/**
 * Session middleware
 * @param {WebModule} webModule - Owner web module
 * @param {string} route - Base route to mount
 * @param {object} options - Options read from web module bootstrap file
 * @param {string} [options.type] - Session type, default as 'cookie'
 * @param {string} [options.name] - Session key name, default as 'nf.sid'
 * @param {boolean} [options.resave] - Forces session to be saved even when unmodified. (default: true)
 * @param {boolean} [options.saveUninitialized] - Forces a session that is "uninitialized" to be saved to the store. A session is uninitialized when it is new but not modified. This is useful for implementing login sessions, reducing server storage usage, or complying with laws that require permission before setting a cookie. (default: true)
 * @param {boolean} [options.proxy] - Trust the reverse proxy when setting secure cookies (via "x-forwarded-proto" header). When set to true, the "x-forwarded-proto" header will be used. When set to false, all headers are ignored. When left unset, will use the "trust proxy" setting from express. (default: undefined)
 * @param {object} [options.store] - Session store options
 * @param {string} [options.store.type] - Session store type, e.g. mongodb
 * @param {object} [options.store.options] - Session store options, e.g. mongodb
 * @param {*} [options.genid] - Function to generate a new session id
 * @param {middlewareFactory} cb
 * @returns {middleware}
 */
module.exports = function (webModule, route, options, cb) {

    options = C._.extend({name: 'nf.sid', resave: true, saveUninitialized: false }, options);

    if (!options.secret) {
        return cb('"secret" option is required for session middleware!');
    }

    if (!options.store && ENV !== 'development') {
        return cb('"store" option is required for non-development env!');
    }

    if (options.store) {
        if (!options.store.type) {
            return cb('"store.type" option is required!');
        }

        var type = options.store.type;
        C.excludeDotDotInPath('middlewares.session.store.type', type);

        if (type === 'mongodb') {
            var MongoDbStore = require('connect-mongo')(expressSession);
            if (options.store.options.db) {

                return C.H.connectDb(options.store.options.db, function (err, db) {
                    if (err) throw err;

                    options.store.options.db = db;
                    options.store = new MongoDbStore(options.store.options);

                    createMiddleware();
                })
            }

            options.store = new MongoDbStore(options.store.options);
        } else {
            return cb('Unsupported session store type: ' + type);
        }
    }

    function createMiddleware() {

        if (!options.type || options.type === 'cookie') {

            return cb(null, expressSession(options));
        } else if (options.type === 'query' || options.type === 'post') {
            var resaveSession = options.resave;
            var saveUninitializedSession = options.saveUninitialized || true;

            var store = options.store || new expressSession.MemoryStore();
            var storeReady = true;

            // generates the new session
            store.generate = function (req) {
                req.sessionID = (options.genid && options.genid(req)) || uid(24);
                req.session = new expressSession.Session(req);
            };

            store.on('disconnect', function () {
                storeReady = false;
            });
            store.on('connect', function () {
                storeReady = true;
            });

            if (options.unset && options.unset !== 'destroy' && options.unset !== 'keep') {
                return cb('"unset" option must be "destroy" or "keep"!');
            }

            var unsetDestroy = options.unset === 'destroy';

            return cb(null, function (req, res, next) {
                // self-awareness
                if (req.session) {
                    C.H.syslog('warn', 'Session object already created.');
                    return next();
                }

                if (!storeReady) {
                    C.H.syslog('warn', 'Session store is not ready and session is disabled in this request.');
                    return next();
                }

                var sessionID, originalHash, originalId;

                // expose store
                req.sessionStore = store;

                // get the session ID
                if (options.type === 'query') {
                    sessionID = req.query[options.name];
                } else {
                    sessionID = req.body[options.name];
                }

                if (options.rehash) {
                    sessionID = C.H.popObject(sessionID);
                }

                req.sessionID = sessionID;

                // proxy end() to commit the session
                var end = res.end;
                var ended = false;
                res.end = function (chunk, encoding) {
                    if (ended) {
                        return false;
                    }

                    if (chunk === undefined) {
                        chunk = '';
                    }

                    ended = true;

                    if (shouldDestroy(req)) {
                        // destroy session
                        store.destroy(req.sessionID, function (err) {
                            if (err) {
                                C.H.syslog('error', 'Destroying session failed.', err.stack);
                            }

                            end.call(res);
                        });

                        return res.write(chunk, encoding);
                    }

                    // no session to save
                    if (!req.session) {
                        return end.call(res, chunk, encoding);
                    }

                    if (shouldSave(req)) {
                        req.session.save(function (err) {
                            if (err) console.error(err.stack);

                            end.call(res);
                        });
                        return res.write(chunk, encoding);
                    }

                    return end.call(res, chunk, encoding);
                };

                // generate a session if the browser doesn't send a sessionID
                if (!req.sessionID) {
                    generate();
                    return next();
                }

                // generate the session object
                store.get(req.sessionID, function (err, sess) {
                    // error handling
                    if (err) {
                        if ('ENOENT' == err.code) {
                            generate();
                            next();
                        } else {
                            next(err);
                        }
                        // no session
                    } else if (!sess) {
                        generate();
                        next();
                        // populate req.session
                    } else {
                        store.createSession(req, sess);
                        originalId = req.sessionID;
                        originalHash = hash(sess);
                        next();
                    }
                });

                // check if session has been modified
                function isModified(sess) {
                    return originalHash != C.H.crc32Hash(sess) || originalId != sess.id;
                }

                // determine if session should be destroyed
                function shouldDestroy(req) {
                    return req.sessionID && unsetDestroy && req.session == null;
                }

                // determine if session should be saved to store
                function shouldSave(req) {
                    return sessionID != req.sessionID
                        ? saveUninitializedSession || isModified(req.session)
                        : resaveSession || isModified(req.session);
                }

                function generate() {
                    store.generate(req);
                    originalId = req.sessionID;
                    originalHash = C.H.crc32Hash(req.session);
                }
            });
        }

        cb('Unsupported session type: ' + options.type);
    }

    createMiddleware();
};

